#include "md5deeper.h"

md5deeper::md5deeper(QWidget *parent)
	: QWidget(parent)
{
	QApplication::setWindowIcon(QIcon("Icons/deep_icon.png"));
	QString openWithFile = "";
	QString cbText = clipboardText();

	if(QCoreApplication::arguments().size() > 1)
		openWithFile = QCoreApplication::arguments().at(1);

	// Instantiate widgets.
	targetFileLabel			= new QLabel("Target File:");
	targetFileText			= new QLineEdit(openWithFile.isEmpty() ?
											"" : openWithFile);
	hashText				= new QLineEdit();
	openTargetFileButton	= new QPushButton("Open...");
	verifyButton			= new QPushButton("Verify");
	saveButton				= new QPushButton("Save to file");
	statusMessageLabel		= new QLabel();
//	timer					= new QTimer(this);
//
//	timer->setSingleShot(true);
//	timer->setInterval(5000);

	// Create the QMap
	hashLengths = QMap<QString, int>();
	hashLengths.insert("md5",		32);
	hashLengths.insert("sha1",		40);
	hashLengths.insert("sha256",	64);
	hashLengths.insert("tiger",		48);
	hashLengths.insert("whirlpool",	128);

	// Create and populate the hash ComboBox
	hashMenu = new QComboBox();
	hashMenu->setInsertPolicy(QComboBox::InsertAlphabetically);
	hashMenu->setSizeAdjustPolicy(QComboBox::AdjustToContents);
	hashMenu->setEditable(false);
	hashMenu->addItem("MD5");
	hashMenu->addItem("SHA1");
	hashMenu->addItem("SHA256");
	hashMenu->addItem("Tiger");
	hashMenu->addItem("Whirlpool");

	// Show buttons.
	openTargetFileButton->show();
	verifyButton->show();
	saveButton->hide();

	// Connect signals to slots.
	connect(openTargetFileButton, SIGNAL(clicked()), this,
			SLOT(openTargetFile()));
	connect(verifyButton, SIGNAL(clicked()), this, SLOT(verify()));
	connect(saveButton, SIGNAL(clicked()), this, SLOT(save()));

//	connect(timer, SIGNAL(timeout()), this, SLOT(clearMessage()));
	connect(targetFileText, SIGNAL(textChanged(QString)), this, SLOT(clearMessage()));
	connect(hashText, SIGNAL(textChanged(QString)), this, SLOT(clearMessage()));
	connect(hashMenu, SIGNAL(activated(int)), this, SLOT(clearMessage()));
	connect(hashText, SIGNAL(textChanged(QString)), this, SLOT(updateHashMenu(QString)));

	// Create layout

	QGridLayout *mainLayout = new QGridLayout();
	mainLayout->addWidget(targetFileLabel,          0, 0, Qt::AlignRight);
	mainLayout->addWidget(targetFileText,           0, 1);
	mainLayout->addWidget(openTargetFileButton,     0, 2);
	mainLayout->addWidget(hashMenu,                 1, 0, Qt::AlignRight);
	mainLayout->addWidget(hashText,                 1, 1);
	mainLayout->addWidget(verifyButton,             1, 2);
	mainLayout->addWidget(statusMessageLabel,		2, 1, Qt::AlignCenter);
	mainLayout->addWidget(saveButton,               2, 2);
	mainLayout->setRowStretch(2, -1);

	// Set layout, window title, and size
	setLayout(mainLayout);
	setWindowTitle("md5deeper");
	setMinimumWidth(460);
//	setMaximumWidth(600);
	setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum);
	move(120, 120);

	QFont thisFont(font());
	thisFont.setPointSize(9);
	setFont(thisFont);

	setAcceptDrops(true);
	targetFileText->setAcceptDrops(false);
	hashText->setAcceptDrops(false);
	statusMessageLabel->setVisible(false);

	// If there is a valid hash in the clipboard, set it to the text field.
	if(validHash(cbText))
		hashText->setText(cbText);

	// If the program was opened with a file,
	if(!(targetFileText->text() == ""))
	{
		// but the hash text field is empty, focus on it
		if(hashText->text() == "")
			hashText->setFocus();
		else
		{
			// Otherwise, both have data, so run verify() and set focus
			// on the save button.
			
			verify();
			saveButton->setFocus();
		}
	}
	else
		targetFileText->setFocus();
}

void md5deeper::openTargetFile()
{
	QString fileName = QFileDialog::getOpenFileName(this,
		"Open Target File",
		QDesktopServices::storageLocation(QDesktopServices::DesktopLocation),
		"All Files (*)");

	if(fileName.isEmpty())	// If no file name is entered, don't run.
		return;
	else
	{										// Otherwise,
		QFile file(fileName);				// create the file object

		if(!file.open(QIODevice::ReadOnly))
		{
			// If open in Read Only fails, display error message and don't run.

			displayMessage(openFileError);
			return;
		}

		// If open in Read Only is successful, set the Target File Name line
		// to the file name.

		targetFileText->setText(file.fileName());
	}
}

void md5deeper::save()
{
	QString fileName = targetFileText->text();
	QString hashValue = hashText->text();

	if(!checkUserErrors())
		return;
	else
	{
		// If no errors, make the output file object and add the hash file
		// extension.

		if(!write(fileName, hashValue))
		{
			// If open is unsuccessful, display an error message and don't run.

			displayMessage(openFileError);
			return;
		}
	}

	// If we've gotten to this point, the write is successful and we can
	// display a message.

	displayMessage(saveSuccess);
}

bool md5deeper::checkUserErrors()
{
	if(targetFileText->text() == "")
	{
		// If file name field is empty, display an error message and
		// dont' run.

		displayMessage(fileNameEmpty);
		return false;
	}
	else if(!validPath(targetFileText->text()))
	{
		// If file name is invalid, display error message and don't run.

		displayMessage(invalidFileName);
		return false;
	}
	else if(hashText->text() == "")
	{
		// If hash value is empty, display error message and don't run.

		displayMessage(hashEmpty);
		return false;
	}
	else if(!(validHash(hashText->text())))
	{
		// Check if it's a valid hash value.

		displayMessage(invalidHash);
		return false;
	}
	else if(!comboMatchesInput())
	{
		// Check if it's a valid hash value.

		displayMessage(invalidHash);
		return false;
	}

	return true;
}

bool md5deeper::comboMatchesInput()
{
	return hashMenu->currentText().toLower() ==
			hashType(hashText->text().trimmed().toLower());
}

bool md5deeper::write(QString origName, QString hashValue)
{
	QString extension = hashMenu->currentText().toLower();
	QFile file(origName + "." + extension);

	if (file.open(QFile::WriteOnly | QFile::Truncate))
	{
		// If open is successful, make a text output stream object.

		QTextStream out(&file);

		// Output the hash value, two spaces, and the file name.

		out << hashValue << " *"
			<< origName.right(origName.size() - origName.lastIndexOf('/') - 1)
			<< "\n";

		return true;
	}
	else
		return false;
}

void md5deeper::displayMessage(statusMessage status)
{
	switch(status)
	{
	case openFileError:
		{
			showMessage("<font color=\"red\">Unable to open file</font>");
		}
		break;

	case fileNameEmpty:
		{
			showMessage("<font color=\"red\">You forgot to add a file!</font>");
		}
		break;

	case hashEmpty:
		{
			showMessage("<font color=\"red\">You forgot to add a hash value!</font>");
		}
		break;

	case invalidHash:
		{
			QString currentHash = hashMenu->currentText();
			showMessage("<font color=\"red\">Not a proper " +
						currentHash + " value!</font>");
		}
		break;

	case invalidFileName:
		{
			showMessage("<font color=\"red\">Invalid file name!</font>");
		}
		break;

	case saveSuccess:
		{
			showMessage("<font color=\"limegreen\">File Generated Successfully!</font>");
		}
		break;

	case hashMatch:
		{
			// Show the save button.
			
			showMessage("<font color=\"limegreen\">Hash Matches!</font>", true);
		}
		break;

	case hashMismatch:
		{
			showMessage("<font color=\"red\">Hash value does not match.</font>");
		}
		break;
	}
}

void md5deeper::displayMessage(QProcess::ProcessError error)
{
	switch(error)
	{
	case QProcess::FailedToStart:
		{
			showMessage("<font color=\"red\">Hashing program failed to start.</font>");
		}
		break;
	case QProcess::Crashed:
		{
			showMessage("<font color=\"red\">Hashing program crashed.</font>");
		}
		break;
	case QProcess::Timedout:
		{
			showMessage("<font color=\"red\">Hashing program timed out.</font>");
		}
		break;
	case QProcess::WriteError:
		{
			showMessage("<font color=\"red\">Input failed.</font>");
		}
		break;
	case QProcess::ReadError:
		{
			showMessage("<font color=\"red\">Reading output failed.</font>");
		}
		break;
	case QProcess::UnknownError:
		{
			showMessage("<font color=\"red\">Unknown error.</font>");
		}
		break;
	}
}

void md5deeper::showMessage(QString message, bool showSaveButton)
{
	if(!(statusMessageLabel->isVisible()))
	{
		statusMessageLabel->show();
		saveButton->setVisible(showSaveButton);
	}

	statusMessageLabel->setText(message);

//	if(startTimer)
//		timer->start(); // clearMessage is called after 5 seconds.
}

void md5deeper::clearMessage()
{
	if(statusMessageLabel->isVisible())
	{
		statusMessageLabel->hide();
		saveButton->hide();
		adjustSize();
	}
}

QString md5deeper::hashType(QString hash) const
{
	int hashLength = hash.trimmed().size();
	return hashLengths.key(hashLength, "");
}

bool md5deeper::validHash(QString hash) const
{
	QString type = hashType(hash);

	// hashType() returns empty QString if argument
	// is not a valid supported hash value
	if(type == "") return false;

	// check if it's a valid hex number with the number of digits its type
	// should have
	QRegExp hex("([0-9a-f]){" + QString().setNum(hash.size()) + "}");
	return hex.exactMatch(hash);
}

bool md5deeper::validPath(QString str) const
{
	// "/valid/path/to/a/file" == true
	return QRegExp("(([A-Za-z]:)?[\\/].+)+").exactMatch(str);
}

void md5deeper::verify()
{
	if(!checkUserErrors())
		return;

	QString userHash = hashText->text(); // Just verified in checkUserErrors(),
										// so no need to trim.
	QString fileName = targetFileText->text().trimmed();

	QProcess deep;
	deep.setReadChannelMode(QProcess::MergedChannels);
	deep.start(hashMenu->currentText().toLower() + "deep",
			   QStringList() << "-q" << fileName);

	if(!deep.waitForFinished())
	{
//		QMessageBox::information(this, "Error", "Error: " + deep.errorString());
//		QErrorMessage::showMessage("WD: " + deep.workingDirectory());
		displayMessage(deep.error());
	}
	else
	{
		QString progHash(deep.readAll().trimmed());
		if(!validHash(progHash))
		{
			int index = progHash.indexOf(fileName);

			if(index == -1)
			{
				progHash.remove(fileName + ": ");
			}
			else
			{
				index += fileName.length() + 2;
				progHash = progHash.mid(index);
			}
			showMessage("<font color=\"red\">" + progHash + "</font>");
		}
		else if(userHash == progHash)
			displayMessage(hashMatch);
		else
			displayMessage(hashMismatch);
	}
}

void md5deeper::updateHashMenu(QString newText)
{
	QString hashVal = newText.trimmed().toLower();

	if(!validHash(hashVal)) return;

	QString hashType = hashLengths.key(hashVal.size(), "");
	if(hashType != "")
	{
		int index = hashMenu->findText(hashType, Qt::MatchFixedString);
		if(index != -1)
			hashMenu->setCurrentIndex(index);
	}
}

QString md5deeper::clipboardText()
{
	const QClipboard *clipboard = QApplication::clipboard();
	const QString cbText = clipboard->text();
	return cbText;
}

void md5deeper::dragEnterEvent(QDragEnterEvent *event)
{
	// If the Drag event's file's MIME data is of type URI List,
	// accept the proposed drag action.

	if (event->mimeData()->hasFormat("text/uri-list"))
		event->acceptProposedAction();
}

void md5deeper::dropEvent(QDropEvent *event)
{
	// Make a QUrl of the Drop event's file's MIME data list's first item.

	QUrl url = event->mimeData()->urls().first();

	// If nothing's in it, return.

	if (url.isEmpty())
		return;

	// Make the fileName into a string. toLocalFile() converts whole path
	// to a QString.

	QString fileName = url.path();

	// If fileName is not empty, set the Target File Line Edit's text as
	// the file name.

	if (!fileName.isEmpty())
		targetFileText->setText(fileName);
}
